package wechat

import (
	"os"
	"path"
	"errors"
	"time"
	"io/ioutil"
	"fmt"
	"bytes"
	"net/http"
	"encoding/json"
	"reflect"
	"mime/multipart"
	"io"
	"encoding/xml"
)

const (
	// environment constants
	UrlPrefix      = "https://Wechat.weixin.qq.com/cgi-bin/"
	MediaUrlPrefix = "http://file.Wechat.weixin.qq.com/cgi-bin/media/"
	retryNum       = 3
)

// message structs
type msgHeader struct {
	XMLName      xml.Name `xml:"xml" json:"-"`
	ToUserName   string   `json:"touser"`
	FromUserName string   `json:"-"`
	CreateTime   int64    `json:"-"`
	MsgType      string   `json:"msgtype"`
}

type textMsg struct {
	msgHeader
	Content string `json:"-"`
	Text struct {
		Content string `xml:"-" json:"content"`
	} `xml:"-" json:"text"`
}

type imageMsg struct {
	msgHeader
	Image struct {
		MediaId string `json:"media_id"`
	} `json:"image"`
}

type voiceMsg struct {
	msgHeader
	Voice struct {
		MediaId string `json:"media_id"`
	} `json:"voice"`
}

type videoMsg struct {
	msgHeader
	Video *Video `json:"video"`
}

type musicMsg struct {
	msgHeader
	Music *Music `json:"music"`
}

type newsMsg struct {
	msgHeader
	ArticleCount int `json:"-"`
	Articles struct {
		Item *[]Article `xml:"item" json:"articles"`
	} `json:"news"`
}

// 群发图文消息
type newsGroupMsg struct {
	Filter struct {
		IsToAll bool   `json:"is_to_all"`
		GroupId string `json:"group_id"`
	} `json:"filter"`

	Mpnews struct {
		MediaId string `json:"media_id"`
	} `json:"mpnews"`
	MsgType string `json:"msgtype"`
}

type Video struct {
	MediaId     string `json:"media_id"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

type Music struct {
	Title        string `json:"title"`
	Description  string `json:"description"`
	MusicUrl     string `json:"musicurl"`
	HQMusicUrl   string `json:"hqmusicurl"`
	ThumbMediaId string `json:"thumb_media_id"`
}

// get fresh access_token string
func (this *Wechat) Fresh(config MpConfig) (string, error) {
	if this.TmpName == "" {
		this.TmpName = config.AppId + "-accesstoken.tmp"
	}
	if this.LckName == "" {
		this.LckName = this.TmpName + ".lck"
	}
	for {
		if this.locked() {
			time.Sleep(time.Second)
			continue
		}
		break
	}
	fi, err := os.Stat(this.TmpName)
	if err != nil && !os.IsExist(err) {
		return this.fetchAndStore(config)
	}
	expires := fi.ModTime().Add(2 * time.Hour).Unix()
	if expires <= time.Now().Unix() {
		return this.fetchAndStore(config)
	}
	tmp, err := os.Open(this.TmpName)
	if err != nil {
		return "", err
	}
	defer tmp.Close()
	data, err := ioutil.ReadAll(tmp)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func (this *Wechat) fetchAndStore(config MpConfig) (string, error) {
	if err := this.lock(); err != nil {
		return "", err
	}
	defer this.unlock()
	token, err := this.fetch(config)
	if err != nil {
		return "", err
	}
	if err := this.store(token); err != nil {
		return "", err
	}
	return token, nil
}

func (this *Wechat) store(token string) error {
	path := path.Dir(this.TmpName)
	fi, err := os.Stat(path)
	if os.IsNotExist(err) {
		if err := os.MkdirAll(path, os.ModePerm); err != nil {
			return err
		}
	}
	if !fi.IsDir() {
		return errors.New("path is not a directory")
	}
	tmp, err := os.OpenFile(this.TmpName, os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		return err
	}
	defer tmp.Close()
	if _, err := tmp.Write([]byte(token)); err != nil {
		return err
	}
	return nil
}

func (this *Wechat) fetch(config MpConfig) (string, error) {
	rtn, err := get(fmt.Sprintf(
		"%stoken?grant_type=client_credential&appid=%s&secret=%s",
		UrlPrefix,
		config.AppId,
		config.Secret,
	))
	if err != nil {
		return "", err
	}
	return rtn.AccessToken, nil
}

func (this *Wechat) unlock() error {
	return os.Remove(this.LckName)
}

func (this *Wechat) lock() error {
	path := path.Dir(this.LckName)
	fi, err := os.Stat(path)
	if os.IsNotExist(err) {
		if err := os.MkdirAll(path, os.ModePerm); err != nil {
			return err
		}
	}
	if !fi.IsDir() {
		return errors.New("path is not a directory")
	}
	lck, err := os.Create(this.LckName)
	if err != nil {
		return err
	}
	lck.Close()
	return nil
}

func (this *Wechat) locked() bool {
	_, err := os.Stat(this.LckName)
	return !os.IsNotExist(err)
}

// send text message
func (this *Wechat) SendTextMsg(accessToken, touser string, content string) error {
	var msg textMsg
	msg.MsgType = "text"
	msg.Text.Content = content
	return this.sendMsg(accessToken, touser, &msg)
}

// send image message
func (this *Wechat) SendImageMsg(accessToken, touser string, mediaId string) error {
	var msg imageMsg
	msg.MsgType = "image"
	msg.Image.MediaId = mediaId
	return this.sendMsg(accessToken, touser, &msg)
}

// send voice message
func (this *Wechat) SendVoiceMsg(accessToken, touser string, mediaId string) error {
	var msg voiceMsg
	msg.MsgType = "voice"
	msg.Voice.MediaId = mediaId
	return this.sendMsg(accessToken, touser, &msg)
}

// send video message
func (this *Wechat) SendVideoMsg(accessToken, touser string, video *Video) error {
	var msg videoMsg
	msg.MsgType = "video"
	msg.Video = video
	return this.sendMsg(accessToken, touser, &msg)
}

// send music message
func (this *Wechat) SendMusicMsg(accessToken, touser string, music *Music) error {
	var msg musicMsg
	msg.MsgType = "music"
	msg.Music = music
	return this.sendMsg(accessToken, touser, &msg)
}

// send news message
func (this *Wechat) SendNewsMsg(accessToken, touser string, articles *[]Article) error {
	var msg newsMsg
	msg.MsgType = "news"
	msg.Articles.Item = articles
	return this.sendMsg(accessToken, touser, &msg)
}

// send message
func (this *Wechat) sendMsg(accessToken, touser string, msg interface{}) error {
	v := reflect.ValueOf(msg).Elem()
	v.FieldByName("ToUserName").SetString(touser)
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	url := fmt.Sprintf("%smessage/custom/send?access_token=", UrlPrefix)
	buf := bytes.NewBuffer(data)
	// retry
	for i := 0; i < retryNum; i++ {
		if _, err := post(url+accessToken, "text/plain", buf); err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		break // success
	}
	return nil
}

// 向全部用户群发图文消息
func (this *Wechat) sendNewsToALl(accessToken, mediaId string) error {
	var news newsGroupMsg
	news.MsgType = "mpnews"
	news.Filter.IsToAll = true
	news.Mpnews.MediaId = mediaId
	return this.sendGroupMsg(accessToken, news)
}

// 向特定GroupId用户群发图文消息
func (this *Wechat) sendNewsToGroup(accessToken, groupId string, mediaId string) error {
	var news newsGroupMsg
	news.MsgType = "mpnews"
	news.Filter.IsToAll = false
	news.Filter.GroupId = groupId
	news.Mpnews.MediaId = mediaId
	return this.sendGroupMsg(accessToken, news)
}

// 群发消息
func (this *Wechat) sendGroupMsg(accessToken string, msg interface{}) error {
	url := fmt.Sprintf("%smessage/mass/sendall?access_token=", UrlPrefix)
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	buf := bytes.NewBuffer(data)
	// retry
	for i := 0; i < retryNum; i++ {
		if _, err := post(url+accessToken, "text/plain", buf); err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		break // success
	}
	return nil
}

type qrScene struct {
	ExpireSeconds int64  `json:"expire_seconds,omitempty"`
	ActionName    string `json:"action_name"`
	ActionInfo struct {
		Scene struct {
			SceneId int64 `json:"scene_id"`
		} `json:"scene"`
	} `json:"action_info"`
}

// get qrcode url
func (this *Wechat) GetQRCodeURL(ticket string) string {
	return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + ticket
}

// create permanent qrcode
func (this *Wechat) CreateQRScene(accessToken string, sceneId int64) (string, error) {
	var inf qrScene
	inf.ActionName = "QR_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(accessToken, &inf)
}

// create temporary qrcode
func (this *Wechat) CreateQRLimitScene(accessToken string, expireSeconds, sceneId int64) (string, error) {
	var inf qrScene
	inf.ExpireSeconds = expireSeconds
	inf.ActionName = "QR_LIMIT_SCENE"
	inf.ActionInfo.Scene.SceneId = sceneId
	return this.createQRCode(accessToken, &inf)
}

func (this *Wechat) createQRCode(accessToken string, inf *qrScene) (string, error) {
	data, err := json.Marshal(inf)
	if err != nil {
		return "", err
	}
	url := fmt.Sprintf("%sqrcode/create?access_token=", UrlPrefix)
	buf := bytes.NewBuffer(data)
	ticket := ""
	// retry
	for i := 0; i < retryNum; i++ {
		rtn, err := post(url+accessToken, "text/plain", buf)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return "", err
		}
		ticket = rtn.Ticket
		break // success
	}
	return ticket, nil
}

// download media to file
func (this *Wechat) DownloadMediaFile(accessToken, mediaId, fileName string) error {
	url := fmt.Sprintf("%sget?media_id=%s&access_token=", MediaUrlPrefix, mediaId)
	// retry
	for i := 0; i < retryNum; i++ {
		resp, err := http.Get(url + accessToken)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		defer resp.Body.Close()
		data, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		// json
		if resp.Header.Get("Content-Type") == "text/plain" {
			var rtn response
			if err := json.Unmarshal(data, &rtn); err != nil {
				if i < retryNum-1 {
					continue
				}
				return err
			}
			if i < retryNum-1 {
				continue
			}
			return errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
		}
		// media
		f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, os.ModePerm)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		defer f.Close()
		if _, err := f.Write(data); err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		break // success
	}
	return nil
}

// upload media to file
func (this *Wechat) UploadMediaFile(accessToken, mediaType, fileName string) (string, error) {
	var buf bytes.Buffer
	bw := multipart.NewWriter(&buf)
	defer bw.Close()
	f, err := os.Open(fileName)
	if err != nil {
		return "", err
	}
	defer f.Close()
	fw, err := bw.CreateFormFile("filename", f.Name())
	if err != nil {
		return "", err
	}
	if _, err := io.Copy(fw, f); err != nil {
		return "", err
	}
	f.Close()
	bw.Close()
	url := fmt.Sprintf("%supload?type=%s&access_token=", MediaUrlPrefix, mediaType)
	mime := bw.FormDataContentType()
	mediaId := ""
	// retry
	for i := 0; i < retryNum; i++ {
		rtn, err := post(url+accessToken, mime, &buf)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return "", err
		}
		mediaId = rtn.MediaId
		break // success
	}
	return mediaId, nil
}

type Button struct {
	Type      string   `json:"type,omitempty"`
	Name      string   `json:"name"`
	Key       string   `json:"key,omitempty"`
	Url       string   `json:"url,omitempty"`
	SubButton []Button `json:"sub_button,omitempty"`
}

// create custom menu
func (this *Wechat) CreateCustomMenu(accessToken string, btn *[]Button) error {
	var menu struct {
		Button *[]Button `json:"button"`
	}
	menu.Button = btn
	data, err := json.Marshal(&menu)
	if err != nil {
		return err
	}
	buf := bytes.NewBuffer(data)
	url := fmt.Sprintf("%smenu/create?access_token=", UrlPrefix)
	// retry
	for i := 0; i < retryNum; i++ {
		if _, err := post(url+accessToken, "text/plain", buf); err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		break // success
	}
	return nil
}

// get custom menu
func (this *Wechat) GetCustomMenu(accessToken string) ([]Button, error) {
	var menu struct {
		Menu struct {
			Button []Button `json:"button"`
		} `json:"menu"`
	}
	url := fmt.Sprintf("%smenu/get?access_token=", UrlPrefix)
	// retry
	for i := 0; i < retryNum; i++ {
		resp, err := http.Get(url + accessToken)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return nil, err
		}
		defer resp.Body.Close()
		data, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return nil, err
		}
		// has error?
		var rtn response
		if err := json.Unmarshal(data, &rtn); err != nil {
			if i < retryNum-1 {
				continue
			}
			return nil, err
		}
		// yes
		if rtn.ErrCode != 0 {
			if i < retryNum-1 {
				continue
			}
			return nil, errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
		}
		// no
		if err := json.Unmarshal(data, &menu); err != nil {
			if i < retryNum-1 {
				continue
			}
			return nil, err
		}
		break // success
	}
	return menu.Menu.Button, nil
}

// delete custom menu
func (this *Wechat) DeleteCustomMenu(accessToken string) error {
	url := UrlPrefix + "menu/delete?access_token="
	// retry
	for i := 0; i < retryNum; i++ {
		if _, err := get(url + accessToken); err != nil {
			if i < retryNum-1 {
				continue
			}
			return err
		}
		break // success
	}
	return nil
}

type UserInfo struct {
	Subscribe     int64  `json:"subscribe"`
	Openid        string `json:"openid"`
	Nickname      string `json:"nickname"`
	Sex           int64  `json:"sex"`
	Language      string `json:"language"`
	City          string `json:"city"`
	Province      string `json:"province"`
	Country       string `json:"country"`
	Headimgurl    string `json:"headimgurl"`
	SubscribeTime int64  `json:"subscribe_time"`
}

// get user info
func (this *Wechat) GetUserInfo(accessToken, openId string) (UserInfo, error) {
	var uinf UserInfo
	url := fmt.Sprintf("%suser/info?lang=zh_CN&openid=%s&access_token=", UrlPrefix, openId)
	// retry
	for i := 0; i < retryNum; i++ {
		resp, err := http.Get(url + accessToken)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return uinf, err
		}
		defer resp.Body.Close()
		data, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			if i < retryNum-1 {
				continue
			}
			return uinf, err
		}
		// has error?
		var rtn response
		if err := json.Unmarshal(data, &rtn); err != nil {
			if i < retryNum-1 {
				continue
			}
			return uinf, err
		}
		// yes
		if rtn.ErrCode != 0 {
			if i < retryNum-1 {
				continue
			}
			return uinf, errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
		}
		// no
		if err := json.Unmarshal(data, &uinf); err != nil {
			if i < retryNum-1 {
				continue
			}
			return uinf, err
		}
		break // success
	}
	return uinf, nil
}

// response from Wechat
type response struct {
	// error fields
	ErrCode int64  `json:"errcode"`
	ErrMsg  string `json:"errmsg"`
	// token fields
	AccessToken string `json:"access_token"`
	ExpiresIn   int64  `json:"expires_in"`
	// media fields
	Type      string `json:"type"`
	MediaId   string `json:"media_id"`
	CreatedAt int64  `json:"created_at"`
	// ticket fields
	Ticket        string `json:"ticket"`
	ExpireSeconds int64  `json:"expire_seconds"`
}

func post(url string, bodyType string, body *bytes.Buffer) (*response, error) {
	resp, err := http.Post(url, bodyType, body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var rtn response
	if err := json.Unmarshal(data, &rtn); err != nil {
		return nil, err
	}
	if rtn.ErrCode != 0 {
		return nil, errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
	}
	return &rtn, nil
}

func get(url string) (*response, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var rtn response
	if err := json.Unmarshal(data, &rtn); err != nil {
		return nil, err
	}
	if rtn.ErrCode != 0 {
		return nil, errors.New(fmt.Sprintf("%d %s", rtn.ErrCode, rtn.ErrMsg))
	}
	return &rtn, nil
}
